/* String streams are the things fed to parsers (which can feed them
 * to a tokenizer if they want). They provide peek and next methods
 * for looking at the current character (next 'consumes' this
 * character, peek does not), and a get method for retrieving all the
 * text that was consumed since the last time get was called.
 *
 * An easy mistake to make is to let a StopIteration exception finish
 * the token stream while there are still characters pending in the
 * string stream (hitting the end of the buffer while parsing a
 * token). To make it easier to detect such errors, the stringstreams
 * throw an exception when this happens.
 */

// Make a stringstream stream out of an iterator that returns strings.
// This is applied to the result of traverseDOM (see codemirror.js),
// and the resulting stream is fed to the parser.
var stringStream = function(source){
  // String that's currently being iterated over.
  var current = "";
  // Position in that string.
  var pos = 0;
  // Accumulator for strings that have been iterated over but not
  // get()-ed yet.
  var accum = "";
  // Make sure there are more characters ready, or throw
  // StopIteration.
  function ensureChars() {
	while (pos == current.length) {
	  accum += current;
	  current = ""; // In case source.next() throws
	  pos = 0;
	  try {current = source.next();}
	  catch (e) {
		if (e != StopIteration) throw e;
		else return false;
	  }
	}
	return true;
  }

  return {
	// peek: -> character
	// Return the next character in the stream.
	peek: function() {
	  if (!ensureChars()) return null;
	  return current.charAt(pos);
	},
	// next: -> character
	// Get the next character, throw StopIteration if at end, check
	// for unused content.
	next: function() {
	  if (!ensureChars()) {
		if (accum.length > 0)
		  throw "End of stringstream reached without emptying buffer ('" + accum + "').";
		else
		  throw StopIteration;
	  }
	  return current.charAt(pos++);
	},
	// get(): -> string
	// Return the characters iterated over since the last call to
	// .get().
	get: function() {
	  var temp = accum;
	  accum = "";
	  if (pos > 0){
		temp += current.slice(0, pos);
		current = current.slice(pos);
		pos = 0;
	  }
	  return temp;
	},
	// Push a string back into the stream.
	push: function(str) {
	  current = current.slice(0, pos) + str + current.slice(pos);
	},
	lookAhead: function(str, consume, skipSpaces, caseInsensitive) {
	  function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
	  str = cased(str);
	  var found = false;

	  var _accum = accum, _pos = pos;
	  if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/);

	  while (true) {
		var end = pos + str.length, left = current.length - pos;
		if (end <= current.length) {
		  found = str == cased(current.slice(pos, end));
		  pos = end;
		  break;
		}
		else if (str.slice(0, left) == cased(current.slice(pos))) {
		  accum += current; current = "";
		  try {current = source.next();}
		  catch (e) {if (e != StopIteration) throw e; break;}
		  pos = 0;
		  str = str.slice(left);
		}
		else {
		  break;
		}
	  }

	  if (!(found && consume)) {
		current = accum.slice(_accum.length) + current;
		pos = _pos;
		accum = _accum;
	  }

	  return found;
	},
	// Wont't match past end of line.
	lookAheadRegex: function(regex, consume) {
	  if (regex.source.charAt(0) != "^")
		throw new Error("Regexps passed to lookAheadRegex must start with ^");

	  // Fetch the rest of the line
	  while (current.indexOf("\n", pos) == -1) {
		try {current += source.next();}
		catch (e) {if (e != StopIteration) throw e; break;}
	  }
	  var matched = current.slice(pos).match(regex);
	  if (matched && consume) pos += matched[0].length;
	  return matched;
	},

	// Utils built on top of the above
	// more: -> boolean
	// Produce true if the stream isn't empty.
	more: function() {
	  return this.peek() !== null;
	},
	applies: function(test) {
	  var next = this.peek();
	  return (next !== null && test(next));
	},
	nextWhile: function(test) {
	  var next;
	  while ((next = this.peek()) !== null && test(next))
		this.next();
	},
	matches: function(re) {
	  var next = this.peek();
	  return (next !== null && re.test(next));
	},
	nextWhileMatches: function(re) {
	  var next;
	  while ((next = this.peek()) !== null && re.test(next))
		this.next();
	},
	equals: function(ch) {
	  return ch === this.peek();
	},
	endOfLine: function() {
	  var next = this.peek();
	  return next == null || next == "\n";
	}
  };
};
